'use strict'

import Comment from './comment'
import Declaration from './declaration'
import Node from './node'
import { isClean, my } from './symbols'

let AtRule, parse, Root, Rule

function cleanSource(nodes) {
    return nodes.map(i => {
        if (i.nodes) {
            i.nodes = cleanSource(i.nodes)
        }
        delete i.source
        return i
    })
}

function markTreeDirty(node) {
    node[isClean] = false
    if (node.proxyOf.nodes) {
        for (let i of node.proxyOf.nodes) {
            markTreeDirty(i)
        }
    }
}

class Container extends Node {
    append(...children) {
        for (let child of children) {
            let nodes = this.normalize(child, this.last)
            for (let node of nodes) {
                this.proxyOf.nodes.push(node)
            }
        }

        this.markDirty()

        return this
    }

    cleanRaws(keepBetween) {
        super.cleanRaws(keepBetween)
        if (this.nodes) {
            for (let node of this.nodes) {
                node.cleanRaws(keepBetween)
            }
        }
    }

    each(callback) {
        if (!this.proxyOf.nodes) {
            return undefined
        }
        let iterator = this.getIterator()

        let index, result
        while (this.indexes[iterator] < this.proxyOf.nodes.length) {
            index = this.indexes[iterator]
            result = callback(this.proxyOf.nodes[index], index)
            if (result === false) {
                break
            }

            this.indexes[iterator] += 1
        }

        delete this.indexes[iterator]
        return result
    }

    every(condition) {
        return this.nodes.every(condition)
    }

    getIterator() {
        if (!this.lastEach) {
            this.lastEach = 0
        }
        if (!this.indexes) {
            this.indexes = {}
        }

        this.lastEach += 1
        let iterator = this.lastEach
        this.indexes[iterator] = 0

        return iterator
    }

    getProxyProcessor() {
        return {
            get(node, prop) {
                if (prop === 'proxyOf') {
                    return node
                } else if (!node[prop]) {
                    return node[prop]
                } else if (
                    prop === 'each' ||
                        (typeof prop === 'string' && prop.startsWith('walk'))
                ) {
                    return (...args) => {
                        return node[prop](
                            ...args.map(i => {
                                if (typeof i === 'function') {
                                    return (child, index) => i(child.toProxy(), index)
                                } else {
                                    return i
                                }
                            })
                        )
                    }
                } else if (prop === 'every' || prop === 'some') {
                    return cb => {
                        return node[prop]((child, ...other) =>
                        cb(child.toProxy(), ...other)
                        )
                    }
                } else if (prop === 'root') {
                    return () => node.root().toProxy()
                } else if (prop === 'nodes') {
                    return node.nodes.map(i => i.toProxy())
                } else if (prop === 'first' || prop === 'last') {
                    return node[prop].toProxy()
                } else {
                    return node[prop]
                }
            },

            set(node, prop, value) {
                if (node[prop] === value) {
                    return true
                }
                node[prop] = value
                if (prop === 'name' || prop === 'params' || prop === 'selector') {
                    node.markDirty()
                }
                return true
            }
        }
    }

    index(child) {
        if (typeof child === 'number') {
            return child
        }
        if (child.proxyOf) {
            child = child.proxyOf
        }
        return this.proxyOf.nodes.indexOf(child)
    }

    insertAfter(exist, add) {
        let existIndex = this.index(exist)
        let nodes = this.normalize(add, this.proxyOf.nodes[existIndex]).reverse()
        existIndex = this.index(exist)
        for (let node of nodes) {
            this.proxyOf.nodes.splice(existIndex + 1, 0, node)
        }

        let index
        for (let id in this.indexes) {
            index = this.indexes[id]
            if (existIndex < index) {
                this.indexes[id] = index + nodes.length
            }
        }

        this.markDirty()

        return this
    }

    insertBefore(exist, add) {
        let existIndex = this.index(exist)
        let type = existIndex === 0 ? 'prepend' : false
        let nodes = this.normalize(
            add,
            this.proxyOf.nodes[existIndex],
            type
        ).reverse()
        existIndex = this.index(exist)
        for (let node of nodes) {
            this.proxyOf.nodes.splice(existIndex, 0, node)
        }

        let index
        for (let id in this.indexes) {
            index = this.indexes[id]
            if (existIndex <= index) {
                this.indexes[id] = index + nodes.length
            }
        }

        this.markDirty()

        return this
    }

    normalize(nodes, sample) {
        if (typeof nodes === 'string') {
            nodes = cleanSource(parse(nodes).nodes)
        } else if (typeof nodes === 'undefined') {
            nodes = []
        } else if (Array.isArray(nodes)) {
            nodes = nodes.slice(0)
            for (let i of nodes) {
                if (i.parent) {
                    i.parent.removeChild(i, 'ignore')
                }
            }
        } else if (nodes.type === 'root' && this.type !== 'document') {
            nodes = nodes.nodes.slice(0)
            for (let i of nodes) {
                if (i.parent) {
                    i.parent.removeChild(i, 'ignore')
                }
            }
        } else if (nodes.type) {
            nodes = [nodes]
        } else if (nodes.prop) {
            if (typeof nodes.value === 'undefined') {
                throw new Error('Value field is missed in node creation')
            } else if (typeof nodes.value !== 'string') {
                nodes.value = String(nodes.value)
            }
            nodes = [new Declaration(nodes)]
        } else if (nodes.selector || nodes.selectors) {
            nodes = [new Rule(nodes)]
        } else if (nodes.name) {
            nodes = [new AtRule(nodes)]
        } else if (nodes.text) {
            nodes = [new Comment(nodes)]
        } else {
            throw new Error('Unknown node type in node creation')
        }

        let processed = nodes.map(i => {
            /* c8 ignore next */
            if (!i[my]) {
                Container.rebuild(i)
            }
            i = i.proxyOf
            if (i.parent) {
                i.parent.removeChild(i)
            }
            if (i[isClean]) {
                markTreeDirty(i)
            }

            if (!i.raws) {
                i.raws = {}
            }
            if (typeof i.raws.before === 'undefined') {
                if (sample && typeof sample.raws.before !== 'undefined') {
                    i.raws.before = sample.raws.before.replace(/\S/g, '')
                }
            }
            i.parent = this.proxyOf
            return i
        })

        return processed
    }

    prepend(...children) {
        children = children.reverse()
        for (let child of children) {
            let nodes = this.normalize(child, this.first, 'prepend').reverse()
            for (let node of nodes) {
                this.proxyOf.nodes.unshift(node)
            }
            for (let id in this.indexes) {
                this.indexes[id] = this.indexes[id] + nodes.length
            }
        }

        this.markDirty()

        return this
    }

    push(child) {
        child.parent = this
        this.proxyOf.nodes.push(child)
        return this
    }

    removeAll() {
        for (let node of this.proxyOf.nodes) {
            node.parent = undefined
        }
        this.proxyOf.nodes = []

        this.markDirty()

        return this
    }

    removeChild(child) {
        child = this.index(child)
        this.proxyOf.nodes[child].parent = undefined
        this.proxyOf.nodes.splice(child, 1)

        let index
        for (let id in this.indexes) {
            index = this.indexes[id]
            if (index >= child) {
                this.indexes[id] = index - 1
            }
        }

        this.markDirty()

        return this
    }

    replaceValues(pattern, opts, callback) {
        if (!callback) {
            callback = opts
            opts = {}
        }

        this.walkDecls(decl => {
            if (opts.props && !opts.props.includes(decl.prop)) {
                return
            }
            if (opts.fast && !decl.value.includes(opts.fast)) {
                return
            }

            decl.value = decl.value.replace(pattern, callback)
        })

        this.markDirty()

        return this
    }

    some(condition) {
        return this.nodes.some(condition)
    }

    walk(callback) {
        return this.each((child, i) => {
            let result
            try {
                result = callback(child, i)
            } catch (e) {
                throw child.addToError(e)
            }
            if (result !== false && child.walk) {
                result = child.walk(callback)
            }

            return result
        })
    }

    walkAtRules(name, callback) {
        if (!callback) {
            callback = name
            return this.walk((child, i) => {
                if (child.type === 'atrule') {
                    return callback(child, i)
                }
            })
        }
        if (name instanceof RegExp) {
            return this.walk((child, i) => {
                if (child.type === 'atrule' && name.test(child.name)) {
                    return callback(child, i)
                }
            })
        }
        return this.walk((child, i) => {
            if (child.type === 'atrule' && child.name === name) {
                return callback(child, i)
            }
        })
    }

    walkComments(callback) {
        return this.walk((child, i) => {
            if (child.type === 'comment') {
                return callback(child, i)
            }
        })
    }

    walkDecls(prop, callback) {
        if (!callback) {
            callback = prop
            return this.walk((child, i) => {
                if (child.type === 'decl') {
                    return callback(child, i)
                }
            })
        }
        if (prop instanceof RegExp) {
            return this.walk((child, i) => {
                if (child.type === 'decl' && prop.test(child.prop)) {
                    return callback(child, i)
                }
            })
        }
        return this.walk((child, i) => {
            if (child.type === 'decl' && child.prop === prop) {
                return callback(child, i)
            }
        })
    }

    walkRules(selector, callback) {
        if (!callback) {
            callback = selector

            return this.walk((child, i) => {
                if (child.type === 'rule') {
                    return callback(child, i)
                }
            })
        }
        if (selector instanceof RegExp) {
            return this.walk((child, i) => {
                if (child.type === 'rule' && selector.test(child.selector)) {
                    return callback(child, i)
                }
            })
        }
        return this.walk((child, i) => {
            if (child.type === 'rule' && child.selector === selector) {
                return callback(child, i)
            }
        })
    }

    get first() {
        if (!this.proxyOf.nodes) {
            return undefined
        }
        return this.proxyOf.nodes[0]
    }

    get last() {
        if (!this.proxyOf.nodes) {
            return undefined
        }
        return this.proxyOf.nodes[this.proxyOf.nodes.length - 1]
    }
}

Container.registerParse = dependant => {
    parse = dependant
}

Container.registerRule = dependant => {
    Rule = dependant
}

Container.registerAtRule = dependant => {
    AtRule = dependant
}

Container.registerRoot = dependant => {
    Root = dependant
}

export { Container }

export default Container

/* c8 ignore start */
Container.rebuild = node => {
    if (node.type === 'atrule') {
        Object.setPrototypeOf(node, AtRule.prototype)
    } else if (node.type === 'rule') {
        Object.setPrototypeOf(node, Rule.prototype)
    } else if (node.type === 'decl') {
        Object.setPrototypeOf(node, Declaration.prototype)
    } else if (node.type === 'comment') {
        Object.setPrototypeOf(node, Comment.prototype)
    } else if (node.type === 'root') {
        Object.setPrototypeOf(node, Root.prototype)
    }

    node[my] = true

    if (node.nodes) {
        node.nodes.forEach(child => {
            Container.rebuild(child)
        })
    }
}
/* c8 ignore stop */
